﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;

namespace MyLib.IO {
	/// <summary>
	/// File system provider.
	/// </summary>
	[DataObject]
	public sealed class FileSystemProvider : DbContextProvider, IFileSystemProvider {
		/// <summary>
		/// Type of temporary files.
		/// </summary>
		public const int TempFile = 0;

		/// <summary>
		/// 
		/// </summary>
		private static IDictionary<int, string> m_folders;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="subFolder"></param>
		public static void RegisterFileType(int fileType, string subFolder) {
			if (m_folders == null)
				m_folders = new Dictionary<int, string>();

			System.Diagnostics.Debug.Assert(fileType > 0);
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(subFolder));
			System.Diagnostics.Debug.Assert(!m_folders.ContainsKey(fileType));

			m_folders.Add(fileType, subFolder);
		}

		/// <summary>
		/// 
		/// </summary>
		internal static void UnregisterFileTypes() {
			if (m_folders != null)
				m_folders.Clear();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="types"></param>
		public static void RegisterFileTypes(IDictionary<int, string> types) {
			System.Diagnostics.Debug.Assert(types != null);

			foreach (var entry in types) {
				RegisterFileType(entry.Key, entry.Value);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="parentId"></param>
		/// <param name="attr"></param>
		/// <returns></returns>
		[DataObjectMethod(DataObjectMethodType.Select)]
		public IEnumerable<FileObject> GetFileObjects(int fileType, string objectId, string parentId = null, FileObjectAttr attr = FileObjectAttr.None | FileObjectAttr.ReadOnly | FileObjectAttr.SubDirectory) {
			const string sql1 = @"select `file_id`, `name`, `file_type`, `object_id`, `size`, `last_write_time`, `description`, `attr` from `fs_file` where `file_type` = #file_type# and `object_id` = #object_id# and `deleted` = 0";
			const string sql2 = @" and (`attr` & #attr#) = `attr`";
			const string sql3 = @" and parent_id = #parent_id#";
			const string sql4 = @" and parent_id is null";

			if (fileType != TempFile && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			var parameterParentId = new DbParameter("parent_id", MyLib.DbParameterType.String, parentId);
			var parameterAttr = new DbParameter("attr", MyLib.DbParameterType.Integer, attr);

			string sql = null;
			if (attr != FileObjectAttr.None) {
				sql = sql1 + sql2;
			} else {
				sql = sql1;
			}
			if (!string.IsNullOrEmpty(parentId)) {
				sql = sql + sql3;
			} else {
				sql = sql + sql4;
			}

			using (var session = SessionFactory.CreateReadOnlySession()) {
				var dt = session.ExecuteQuery(
					sql,
					System.Data.CommandType.Text,
					parameterFileType, parameterObjectId, parameterAttr, parameterParentId);

				foreach (System.Data.DataRow row in dt.Rows) {
					yield return new FileObject(
						row.GetString(1),
						row.GetString(2),
						row.GetInt32(3),
						row.GetString(4),
						row.GetInt64(5),
						row.GetDateTime(6),
						row.GetString(7),
						"",
						(FileObjectAttr)row.GetInt32(8)
						);
				} // end of foreach.
			} // end of using.
		}

		/// <summary>
		/// Move files.
		/// </summary>
		/// <param name="srcFileType"></param>
		/// <param name="srcObjectId"></param>
		/// <param name="dstFileType"></param>
		/// <param name="dstObjectId"></param>
		/// <returns></returns>
		[DataObjectMethod(DataObjectMethodType.Update)]
		public int MoveFileObjects(int srcFileType, string srcObjectId, int dstFileType, string dstObjectId) {
			if (srcFileType != TempFile && !m_folders.ContainsKey(srcFileType))
				throw new ArgumentOutOfRangeException("srcFileType", srcFileType,
					string.Format("FileType {0} has not been registerd.", srcFileType));

			if (!m_folders.ContainsKey(dstFileType))
				throw new ArgumentOutOfRangeException("dstFileType", dstFileType,
					string.Format("FileType {0} has not been registerd.", dstFileType));

			var parameterSrcFileType = new DbParameter("src_file_type", MyLib.DbParameterType.Integer, srcFileType);
			var parameterSrcObjectId = new DbParameter("src_object_id", MyLib.DbParameterType.String, srcObjectId);
			var parameterDstFileType = new DbParameter("dst_file_type", MyLib.DbParameterType.Integer, dstFileType);
			var parameterDstObjectId = new DbParameter("dst_object_id", MyLib.DbParameterType.String, dstObjectId);

			using (var session = SessionFactory.CreateSession()) {
				var ret = session.ExecuteUpdate(
				"update fs_file set file_type = #dst_file_type#, `object_id` = #dst_object_id# where file_type = #src_file_type# and `object_id` = #src_object_id#",
					System.Data.CommandType.Text,
					parameterSrcFileType, parameterSrcObjectId,
					parameterDstFileType, parameterDstObjectId);

				session.Commit();

				return ret;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileId"></param>
		/// <returns></returns>
		[DataObjectMethod(DataObjectMethodType.Select)]
		public FileObject GetFileObject(string fileId) {
			if (string.IsNullOrWhiteSpace(fileId))
				throw new ArgumentException("Parameter \"fileId\" cannot be null or empty.", "fileId");

			var parameters = new DbParameter[1];
			parameters[0] = new DbParameter("file_id", MyLib.DbParameterType.String, fileId);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				var dt = session.ExecuteSingleQuery(
					"select file_id, `name`, `file_type`, `object_id`, `size`, `last_write_time`, `description`, `summary`, `attr` from fs_file where file_id = #file_id# and `deleted` = 0",
					System.Data.CommandType.Text,
					parameters);

				if (dt.Rows.Count > 0) {
					var row = dt.Rows[0];

					return new FileObject(
						row.GetString(1),
						row.GetString(2),
						row.GetInt32(3),
						row.GetString(4),
						row.GetInt64(5),
						row.GetDateTime(6),
						row.GetString(7),
						row.GetString(8),
						(FileObjectAttr)row.GetInt32(9)
						);
				} else {
					return null;
				}
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		/// <param name="parentId"></param>
		/// <returns></returns>
		[DataObjectMethod(DataObjectMethodType.Select)]
		public FileObject GetFileObject(int fileType, string objectId, string fileName, string parentId = null) {
			const string sql1 = @"select `file_id`, `name`, `file_type`, `object_id`, `size`, `last_write_time`, `description`, `summary`, `attr` from fs_file where file_type = #file_type# and object_id = #object_id# and `name` = #name# and `deleted` = 0";
			const string sql2 = @" and parent_id = #parent_id#";
			const string sql3 = @" and parent_id is null";

			if (fileType != TempFile && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			var parameterName = new DbParameter("name", MyLib.DbParameterType.String, fileName);
			var parameterParentId = new DbParameter("parent_id", MyLib.DbParameterType.String, parentId);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				var dt = session.ExecuteSingleQuery(
					string.IsNullOrEmpty(parentId) ? sql1 + sql3 : sql1 + sql2,
					System.Data.CommandType.Text,
					parameterFileType, parameterObjectId, parameterName, parameterParentId);

				if (dt.Rows.Count > 0) {
					var row = dt.Rows[0];

					return new FileObject(
						row.GetString(1),
						row.GetString(2),
						row.GetInt32(3),
						row.GetString(4),
						row.GetInt64(5),
						row.GetDateTime(6),
						row.GetString(7),
						row.GetString(8),
						(FileObjectAttr)row.GetInt32(9)
						);
				} else {
					return null;
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileId"></param>
		/// <returns></returns>
		public byte[] GetFileObjectContent(string fileId) {
			const string sql1 = @"select `content` from `fs_file_content` fc
join `fs_file` f on f.`file_id` = fc.`file_id` 
where f.`file_id` = #file_id# and f.`deleted` = 0";

			var parameterFileId = new DbParameter("file_id", DbParameterType.String, fileId);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				using (var reader = session.ExecuteReader(
					sql1,
					System.Data.CommandType.Text,
					parameterFileId)) {

					if (reader.Read()) {
						return GetBytes(reader, 0);
					} else {
						return new byte[0];
					}
				} // end of using reader.
			} // end of using session.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="session"></param>
		/// <param name="parameterFileId"></param>
		/// <param name="parameterParentId"></param>
		private void UpdateFileObjectPath(Session session, DbParameter parameterFileId, DbParameter parameterParentId) {
			const string sql1 = "delete from fs_file_path where d_id = #file_id#";

			const string sql2 = @"insert into fs_file_path(`a_id`, `d_id`) 
select `file_id`, `file_id` from fs_file
where `file_id` = #file_id#";

			const string sql3 = @"insert into fs_file_path(`a_id`, `d_id`) 
select fp.`a_id`, f.`file_id` from fs_file_path fp
left join fs_file f on 1 = 1
where fp.`d_id` = #parent_id# and f.`file_id` = #file_id#";

			session.ExecuteUpdate(sql1, System.Data.CommandType.Text, parameterFileId);
			session.ExecuteUpdate(sql2, System.Data.CommandType.Text, parameterFileId);
			session.ExecuteUpdate(sql3, System.Data.CommandType.Text, parameterFileId, parameterParentId);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		/// <param name="content"></param>
		/// <param name="lastWriteTime"></param>
		/// <param name="description"></param>
		/// <param name="parentId"></param>
		/// <param name="summary"></param>
		/// <param name="attr"></param>
		/// <returns></returns>
		[DataObjectMethod(DataObjectMethodType.Update)]
		public int SetFileObject(int fileType, string objectId, string fileName, byte[] content, DateTime lastWriteTime, string description = "", string summary = "", string parentId = null, FileObjectAttr attr = FileObjectAttr.None) {
			const string sql1 = @"select file_id from fs_file where file_type = #file_type# and `object_id` = #object_id# and `name` = #name# and `deleted` = 0";
			const string sql2 = @" and parent_id = #parent_id#";
			const string sql3 = @" and parent_id is null";
			const string sql4 = @"insert into fs_file (`file_id`, `file_type`, `object_id`, `name`, `size`, `last_write_time`, `parent_id`, `description`, `summary`, `attr`) values(#file_id#, #file_type#, #object_id#, #name#, #size#, #last_write_time#, #parent_id#, #description#, #summary#, #attr#)";
			const string sql5 = @"update fs_file set `file_type` = #file_type#, `object_id` = #object_id#, `name` = #name#, `size` = #size#, `last_write_time` = #last_write_time#, `parent_id` = #parent_id#, `description` = #description#, `summary` = #summary#, `attr` = #attr# where `file_id` = #file_id#";
			const string sql6 = @"insert into fs_file_content (file_id, `content`) values(#file_id#, #content#)";
			const string sql7 = @"update fs_file_content set `content` = #content# where `file_id` = #file_id#";

			if (fileType != TempFile && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			if (description == null) {
				description = "";
			} else {
				description = description.Trim();
			}
			if (summary == null) {
				summary = "";
			} else {
				summary = summary.Trim();
			}
			if (content == null) {
				content = new byte[0];
			}

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			var parameterFileName = new DbParameter("name", MyLib.DbParameterType.String, fileName);
			var parameterDescription = new DbParameter("description", MyLib.DbParameterType.String, description);
			var parameterSummary = new DbParameter("summary", MyLib.DbParameterType.String, summary);
			var parameterFileId = new DbParameter("file_id", MyLib.DbParameterType.String, "");
			var parameterSize = new DbParameter("size", MyLib.DbParameterType.Integer, content.Length);
			var parameterContent = new DbParameter("content", MyLib.DbParameterType.Binary, content);
			var parameterLastWriteTime = new DbParameter("last_write_time", MyLib.DbParameterType.DateTime, lastWriteTime);
			var parameterAttr = new DbParameter("attr", MyLib.DbParameterType.Integer, attr);
			var parameterParentId = new DbParameter("parent_id", MyLib.DbParameterType.String, parentId);

			int c = 0;

			using (var session = SessionFactory.CreateSession()) {
				var fileId = session.ExecuteScalar<string>(
					string.IsNullOrEmpty(parentId) ? sql1 + sql3 : sql1 + sql2,
					System.Data.CommandType.Text,
					parameterFileType,
					parameterObjectId,
					parameterFileName,
					parameterParentId);

				if (string.IsNullOrEmpty(fileId)) {
					// Insert.
					parameterFileId.Value = Guid.NewGuid().ToString("N");

					c = session.ExecuteUpdate(sql4, System.Data.CommandType.Text,
						parameterFileType, parameterObjectId, parameterFileName,
						parameterFileId, parameterSize, parameterLastWriteTime, parameterParentId,
						parameterDescription, parameterSummary, parameterAttr);

					session.ExecuteUpdate(sql6, System.Data.CommandType.Text,
						parameterFileId, parameterContent);
				} else {
					// Update
					parameterFileId.Value = fileId;

					c = session.ExecuteUpdate(sql5, System.Data.CommandType.Text,
						parameterFileType, parameterObjectId, parameterFileName,
						parameterFileId, parameterSize, parameterLastWriteTime, parameterParentId,
						parameterDescription, parameterSummary, parameterAttr);

					session.ExecuteUpdate(sql7, System.Data.CommandType.Text,
						parameterFileId, parameterContent);
				}

				UpdateFileObjectPath(session, parameterFileId, parameterParentId);

				session.Commit();

				return c;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileId"></param>
		[DataObjectMethod(DataObjectMethodType.Delete)]
		public int DeleteFileObject(string fileId) {
			if (string.IsNullOrEmpty(fileId))
				throw new ArgumentNullException("fileId");

			var parameterFileId = new DbParameter("file_id", MyLib.DbParameterType.String, fileId);

			using (var session = SessionFactory.CreateSession()) {
				var c = session.ExecuteUpdate(
					"update `fs_file` set `deleted` = 1 where `file_id` = #file_id# and `deleted` = 0",
					System.Data.CommandType.Text,
					parameterFileId);

				session.Commit();

				return c;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		/// <param name="parentId"></param>
		[DataObjectMethod(DataObjectMethodType.Delete)]
		public int DeleteFileObject(int fileType, string objectId, string fileName, string parentId = null) {
			const string sql1 = @"update `fs_file` set `deleted` = 1 where `file_type` = #file_type# and `object_id` = #object_id# and `name` = #name# and `deleted` = 0";
			const string sql2 = @" and parent_id = #parent_id#";
			const string sql3 = @" and parent_id is null";

			if (!m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			var parameterName = new DbParameter("name", MyLib.DbParameterType.String, fileName);
			var parameterParentId = new DbParameter("parent_id", MyLib.DbParameterType.String, parentId);

			using (var session = SessionFactory.CreateSession()) {
				var c = session.ExecuteUpdate(
					string.IsNullOrEmpty(parentId) ? sql1 + sql3 : sql1 + sql2,
					System.Data.CommandType.Text,
					parameterFileType, parameterObjectId, parameterName, parameterParentId);

				session.Commit();

				return c;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		[DataObjectMethod(DataObjectMethodType.Delete)]
		public int DeleteAllFileObject(int fileType, string objectId) {
			if (!m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);

			using (var session = SessionFactory.CreateSession()) {
				var c = session.ExecuteUpdate(
					"update `fs_file` set `deleted` = 1 where file_type = #file_type# and `object_id` = #object_id# and `deleted` = 0",
					System.Data.CommandType.Text,
					parameterFileType, parameterObjectId);

				session.Commit();

				return c;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="reader"></param>
		/// <param name="index"></param>
		/// <returns></returns>
		private static byte[] GetBytes(System.Data.Common.DbDataReader reader, int index) {
			const int ChunkSize = 2 * 1024;

			byte[] buffer = new byte[ChunkSize];
			long bytesReadInTotal = 0;

			using (MemoryStream stream = new MemoryStream()) {
				long len = reader.GetBytes(index, 0, null, 0, 0);

				while (bytesReadInTotal < len) {
					var bytesRead = reader.GetBytes(index, bytesReadInTotal, buffer, 0, buffer.Length);
					bytesReadInTotal += bytesRead;

					stream.Write(buffer, 0, (int)bytesRead);
				}

				stream.Flush();
				return stream.ToArray();
			} // end of using.
		}

	} // end of FileSystemProvider.
}
